package unit;

import hospital.Hospital;

import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;

import movement.Location;

import dispatch.Clock;
import emergency.*;

/**
 * A class for creating Unit with an currentLocation, homeLocation, name,
 * emergency and assigned to see whether this unit is available for an emergency
 * 
 */
public abstract class Unit implements Observer {

	/**
	 * Variable registering this unit's current location
	 */
	private Location currentLocation;

	/**
	 * Variable registering this unit's home location
	 */
	private Garage homeLocation;

	/**
	 * Variable registering the location where the unit is heading for
	 */
	private Location targetLocation;

	/**
	 * Variable registering this unit's name
	 */
	private String name;

	/**
	 * Variable registering this unit's current emergency
	 */
	private Emergency emergency;

	/**
	 * Variable registering this unit's moving speed
	 */
	private int speed;

	/**
	 * Variable registering when the unit is broken
	 */
	private boolean broken;

	/**
	 * Initialize this new unit with given name,speed and garage
	 * 
	 * @param name
	 *            The name of this Unit
	 * 
	 * @param speed
	 *            The moving speed of this Unit
	 * 
	 * @param garage
	 *            The garage of this Unit
	 * 
	 * @post This new unit's name is set to the given name
	 * 
	 * @post This new unit's homeLocation is set to the given garage
	 * 
	 * @post This new unit's currentLocation is set to the garage it's location
	 * 
	 * @post This new unit's targetLocation is set to the null
	 * 
	 * @post This new unit's emergency is set to null ( not assigned to
	 *       emergency )
	 * 
	 * @post This unit it's speed is set to the given speed
	 * 
	 * @post This new unit's state ( broken / intact ) is set to intact
	 * 
	 * @post This unit is added to the given garage
	 * 
	 * @post This unit add's itself to the clock
	 * 
	 * @throws IllegalArgumentException
	 *             If the name already occurs in the garage, or name is null
	 * 
	 * @throws IllegalArgumentException
	 *             If the name of this unit is already in the garage, or the
	 *             given name is null
	 * 
	 * */
	public Unit(String name, int speed, Garage garage) {
		if (canHaveAsGarage(garage)) {
			setHomeLocation(garage);
			setCurrentLocation(garage.getLocation());
		} else {
			throw new IllegalArgumentException("The garage has to be valid");
		}
		setTargetLocation(null);
		setEmergency(null);
		setSpeed(speed);
		setBroken(false);
		if (canHaveAsName(name, garage)) {
			setName(name);
			garage.addUnit(this);
		} else {
			throw new IllegalArgumentException(
					"The name of this unit already exists in this garage, or the name is null");
		}
		garage.getDispatchcenter().getClock().addObserver(this);
	}

	/**
	 * Method for assigning the given emergency to this unit
	 * 
	 * @param emergency
	 *            The emergency where this unit needs to be assigned to
	 * 
	 * @post The emergency of this unit is set to the given emergency if this
	 *       unit canBeAssigned and the emergency needs this unit
	 * 
	 * @post The unit of emergency is set to this unit if this unit
	 *       canBeAssigned and the emergency needs this unit
	 */
	public void assignEmergency(Emergency emergency) {
		if (canBeAssigned()) {
			setEmergency(emergency);
			emergency.assignUnit(this);
		}
	}

	/**
	 * Abstract method for assign an Hospital to a unit
	 * 
	 * @param h
	 *            The hospital you want to assign to this unit
	 */
	public abstract void assignHospital(Hospital h);

	/**
	 * Method for breaking this unit down
	 * 
	 * @post This unit is broken down
	 * 
	 * @post This unit has no more emergency, except when there's no new unit of
	 *       the same type is available
	 * 
	 * @post This unit stays on it's currentlocation
	 * 
	 * @throws IllegalArgumentException
	 *             , if this unit already broke down
	 */
	public void brokeDown() {
		if (isBrokenDown()) {
			throw new IllegalArgumentException(
					"This unit is already broken down");
		} else {
			setTargetLocation((Location) getCurrentLocation().clone());
			getTargetLocation().setName(" BROKEN ");
			setBroken(true);
			if (getEmergency() != null) { //bezig met een emergency
				if (getEmergency().getDisaster() != null) { // de emergency waaraan de unit werkt is onderdeel van een disaster
					Disaster d = (Disaster)getEmergency().getDisaster();
					getEmergency().unitBrokeDown(this);
					d.getUnits().add(this);
			
						// Er moet een vervanging gezocht worden!
						Boolean canBeSolved = false;
						try{
							getEmergency().createSuggestion(d.getAvailableUnitsClone());
							canBeSolved=true;
						}
						catch(Exception e){
							//kan niet opgelost worden met de huidige units
						}
						if(!canBeSolved){
							try{
								ArrayList<Unit> newUnits = getEmergency().createSuggestion(getHomeLocation().getAvailableUnits());
								for(Unit u :newUnits)
									d.getUnits().add(u);
								canBeSolved=true;
							}
							catch(Exception e){
								//kan ook niet opgelost worden met de garage
							}
						}
						if(canBeSolved){
							d.dispatchUnits();
						}
					setEmergency(d);
					
				} else if (unitOfSameTypeAvailable()) {
					getEmergency().unitBrokeDown(this);
					setEmergency(null);
				}
			}
		}
	}

	/**
	 * Method for checking a Unit can be assigned to an emergency or not
	 * 
	 * @return Returns true if this unit can be assigned (not yet assigned and
	 *         not broken) to an emergency, false if this unit can't be assigned
	 *         to an emergency
	 */
	public boolean canBeAssigned() {
		if ((getEmergency() == null || getEmergency().getEmergenciesClone() != null)
				&& isBroken() == false)
			return true;
		else {
			return false;
		}
	}

	/**
	 * Method for checking a Unit can be assigned to the given emergency or not
	 * 
	 * @return Returns true if this unit can be assigned (not yet assigned and
	 *         not broken) to the given emergency, false if this unit can't be
	 *         assigned to the given emergency
	 */

	public boolean canBeAssigned(Emergency e) {
		if (getEmergency() == e && isBroken() == false)
			return true;
		else {
			throw new IllegalArgumentException("unit cannot be assigned!");
		}
	}

	/**
	 * Method for checking this Unit has a valid garage
	 * 
	 * @return Returns false if the garage is null, true otherwise
	 */
	private boolean canHaveAsGarage(Garage g) {
		return (g != null);
	}

	/**
	 * Method for checking the given location is valid
	 * 
	 * @return Returns false if the location is null, true otherwise
	 */
	private boolean canHaveAsLocation(Location l) {
		return (l != null);
	}

	/**
	 * Method for checking this Unit has a valid name
	 * 
	 * @return Returns false if the name is null or the name of this unit
	 *         already occurs in the given garage, true otherwise
	 */
	private boolean canHaveAsName(String name, Garage garage) {
		if (name == null || garage.nameAlreadyOccurs(name))
			return false;

		return true;
	}

	/**
	 * Method for checking this Unit has a valid speed, which has to be 10 or
	 * more
	 * 
	 * @return Returns false if the speeds is < 10, true otherwise
	 */
	private boolean canHaveAsSpeed(int s) {
		return (s >= 10);
	}

	/**
	 * Method for checking this unit can finish it's emergency (= be at the
	 * location of the emergency) or not
	 * 
	 * @return Returns true if the emergency can be finished, false otherwise
	 */
	public boolean emergencyCanBeFinished() {
		if (getEmergency() == null)
			return false;
		else {
			if (isAtEmergency() || isBroken()){
				return true;
			}
		}
		return false;
	}

	/**
	 * Method to indicate this unit has finished its emergency
	 * 
	 * @post The emergency is set completed, so set to null
	 * 
	 * @post The target location is set to the home location
	 * 
	 * @post This unit is removed from the emergency it's unit list
	 * 
	 * @throws IllegalArgumentException
	 *             , if the unit needs to wait for units of the same type
	 * 
	 * @throws IllegalArgumentException
	 *             , if the unit is not on the emergency
	 * 
	 * @throws IllegalArgumentException
	 *             , if the unit has no emergency assigned
	 * 
	 * */
	public void endEmergency() {
		if (getEmergency() != null) {
			if (emergencyCanBeFinished()) {
				if (this.unitsOfSameTypeOnEmergency()) {
					// unit vraagt aan emergency wat te doen als hij klaar is
					emergency.unitCompletedEmergency(this);
				} else {
					throw new IllegalArgumentException(
							"This unit needs to wait for units of the same type");
				}
			} else {
				throw new IllegalArgumentException(
						"This unit is not where he has to be");
			}
		} else {
			throw new IllegalArgumentException(
					"This unit is not yet assigned to an emergency");
		}
	}

	/**
	 * Method for checking this Unit is equals to the given Unit
	 * 
	 * @param unit
	 *            The unit you want to compare this Unit with
	 * 
	 * @return Returns true if both units are equal, false otherwise
	 */
	public boolean equals(Unit unit) {
		boolean t = false;
		if (this.getName().equals(unit.getName()))
			if (this.getCurrentLocation().equals(unit.getCurrentLocation()))
				t = true;

		return t;
	}

	/**
	 * Method for revoking this unit without constraints. unit will always be
	 * revoked, if it has an emergency
	 * 
	 * @post This unit is released from it's emergency
	 * 
	 * @post This unit is heading for it's homelocation
	 * 
	 * @throws IllegalArgumentException
	 *             , if the unit doesn't have an emergency
	 */
	public void forceRevokeUnit() {
		try {
			getEmergency().removeUnit(this);
			setEmergency(null);
			setTargetLocation(getHomeLocation().getLocation());
		} catch (Exception e) {
			throw new IllegalArgumentException(
					"This unit can't be force-revoked");
		}
	}

	/**
	 * Getter for getting this unit's status (broken or not)
	 * 
	 * @return Returns true if the unit is broken false if the unit isn't broken
	 */
	public boolean getBroken() {
		return broken;
	}

	/**
	 * Getter for getting this unit's location
	 * 
	 * @return Returns the current currentLocation of this Unit
	 */
	public Location getCurrentLocation() {
		return currentLocation;
	}

	/**
	 * Getter for getting this unit's emergency
	 * 
	 * @return Returns current emergency of this Unit
	 */
	public Emergency getEmergency() {
		return emergency;
	}

	/**
	 * Getter for getting this unit's homeLocation
	 * 
	 * @return Returns the homeLocation of this Unit
	 */
	public Garage getHomeLocation() {
		return homeLocation;
	}

	/**
	 * Getter for getting this unit's name
	 * 
	 * @return Returns name of this Unit
	 */
	public String getName() {
		return name;
	}

	/**
	 * Getter for the speed of this unit
	 * 
	 * @return Returns current speed of this Unit
	 */
	public int getSpeed() {
		return speed;
	}

	/**
	 * Getter for getting this unit's targetLocation
	 * 
	 * @return Returns the current targetLocation of this Unit
	 */
	public Location getTargetLocation() {
		return targetLocation;
	}

	/**
	 * Getter for getting the hospital this unit is going to (if any)
	 * 
	 * @return Returns the hospital of this Unit
	 */
	public Hospital getToHospital() {
		return null;
	}

	/**
	 * Getter for getting this unit's occupation (assigned or not)
	 * 
	 * @return Returns boolean
	 */
	public boolean isAssigned() {
		return this.getEmergency() != null;
	}

	/**
	 * Method for checking this unit is at it emergency
	 * 
	 * @return Returns true if this unit is at it's assigned emergency, false if
	 *         not
	 * 
	 * @throws IllegalArgumentException
	 *             , if this unit hasn't got an emergency
	 */
	public boolean isAtEmergency() {
		if (getEmergency() == null)
			throw new IllegalArgumentException(
					"This unit doesn't have an emergency to be on");

		return getEmergency().getLocation().equals(getCurrentLocation());
	}

	/**
	 * Method for checking if this Unit is broken
	 * 
	 * @return Return true if this unit is broken, false when intact
	 */
	public boolean isBroken() {
		return (this.broken);
	}

	/**
	 * Method for checking this unit is broke down
	 * 
	 * @return Returns true when this unit is broken down, false when intact
	 */
	private boolean isBrokenDown() {
		return broken;
	}

	/**
	 * Method for moving this unit
	 * 
	 * @param timeDif
	 *            The amount of minutes the unit has to traveled/drive
	 * 
	 * @post the current location of this unit is updated
	 */
	private void move(int timeDif) {
		if (targetLocation != null) {
			double kmMin = (double) getSpeed() / 60;
			double possibleDist = timeDif * kmMin; // distance * km per minute
			double distToGo = currentLocation.distance(targetLocation);
			if (distToGo < possibleDist) { // if we reach destination
				this.setCurrentLocation(this.getTargetLocation());
				targetLocation = null;
			} else {
				this.getCurrentLocation().changeLocation(
						this.getTargetLocation(), possibleDist);
			}
		}
	}

	public void releaseFromDisaster() {
		setEmergency(null);
		setTargetLocation(getHomeLocation().getLocation());
	}

	/**
	 * Method to repair this unit
	 * 
	 * @post This unit is repaired
	 * 
	 * @post This unit it's targetlocation is set to home when it has no
	 *       emergency or set to the location of the emergency
	 * 
	 * @throws IllegalArgumentException
	 *             , if this unit isn't broken down
	 */
	public void repair() {
		if (!isBrokenDown())
			throw new IllegalArgumentException("This unit isn't broken down");
		else {
			setBroken(false);
			if (getEmergency() == null)
				setTargetLocation(getHomeLocation().getLocation());
			else
				setTargetLocation(getEmergency().getLocation());
		}
	}

	/**
	 * Method for revoking this unit.
	 * 
	 * @post This unit is released from it's emergency if the unit is at the
	 *       emergency
	 * 
	 * @post This unit is heading for it's homelocation
	 * 
	 * @throws IllegalAccessException
	 *             , if the revoke can't be done
	 */
	public void revokeUnit() throws IllegalAccessException {
		if (isAtEmergency()) {
			throw new IllegalAccessException(this.getName() + " is not revoked");
		} else {
			if(getEmergency().getDisaster()!=null){ //is part of disaster
				getEmergency().getDisaster().markToRevoke(this);
			}
			else{
				forceRevokeUnit();
			}
		}
	}

	/**
	 * Method for setting this Unit it's state ( broken or not broken )
	 * 
	 * @param broken
	 *            The status that the unit will get (broken or intact)
	 * 
	 * @post The status of this unit is set to the given broken-status
	 */
	private void setBroken(boolean broken) {
		this.broken = broken;
	}

	/**
	 * Method for setting this unit's currentLocation
	 * 
	 * @param currentLocation
	 *            The location that needs to be set to current location
	 * 
	 * @post The currentLocation of this unit is changed to the given location
	 * @post If the unit is set on a place different from the home location, and
	 *       has no target, his target will be his homelLcation
	 * 
	 * @throws IllegalArgumentException
	 *             , if the given currentLocation is null
	 */
	public void setCurrentLocation(Location currentLocation) {
		if (canHaveAsLocation(currentLocation)) {
			Location newloc = (Location) currentLocation.clone();
			this.currentLocation = newloc;
			if (getTargetLocation() == null
					&& getCurrentLocation() != getHomeLocation().getLocation()) {
				setTargetLocation(homeLocation.getLocation());
			}
		} else
			throw new IllegalArgumentException(
					"CurrentLocation may not be null!");
	}

	/**
	 * Method for setting this unit's emergency
	 * 
	 * @param emergency
	 *            The emergency that needs to be set to emergency of this unit
	 * 
	 * @post The emergency of this unit is changed to the given emergency
	 */
	private void setEmergency(Emergency emergency) {
		this.emergency = emergency;
	}

	/**
	 * Method for setting this unit's homeLocation
	 * 
	 * @param homeLocation
	 *            The garage that needs to be set to home location
	 * 
	 * @post The homeLocation of this unit is changed to the given garage
	 * 
	 * @throws IllegalArgumentException
	 *             , if the given homeLocation is null
	 */
	public void setHomeLocation(Garage homeLocation) {
		if (canHaveAsGarage(homeLocation))
			this.homeLocation = homeLocation;
		else
			throw new IllegalArgumentException("Homelocation may not be null!");
	}

	/**
	 * Method for setting this unit's name
	 * 
	 * @param name
	 *            The name that needs to be set to name of this unit
	 * 
	 * @post The name of this unit is changed to the given name
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Method for setting this unit's speed
	 * 
	 * @param speed
	 *            The speed that needs to be set to speed of this unit
	 * 
	 * @post The speed of this unit is changed to the given speed
	 * 
	 * @post If the given speed is less than 10 , speed is set to 10
	 */
	public void setSpeed(int speed) {
		if (canHaveAsSpeed(speed))
			this.speed = speed;
		else
			this.speed = 10;
	}

	/**
	 * Method for setting this unit's targetLocation
	 * 
	 * @param targetlocation
	 *            The location that needs to be set as target location
	 * 
	 * @post The targetLocation of this unit is changed to the given location
	 */
	public void setTargetLocation(Location targetLocation) {
		this.targetLocation = targetLocation;
	}

	/**
	 * Abstract method for returning a string representation of this unit, with
	 * all info
	 * 
	 * @return The string representation of this unit with all the info,
	 *         depending on the subclass Unit
	 */
	public abstract String showInfo();

	/**
	 * Abstract method for returning a string representation of this unit
	 * 
	 * @return The string representation of this unit, depending on the subclass
	 *         Unit
	 */
	public abstract String toString();

	/**
	 * Method for checking a Unit of the same type is available to Dispatch
	 * 
	 * @throws IllegalArgumentException
	 *             , if this unit is not assigned to an emergency
	 * 
	 * @return Return true if another unit of the same type is available, false
	 *         otherwise
	 */
	public boolean unitOfSameTypeAvailable() {
		if (getEmergency() == null)
			throw new IllegalArgumentException(
					"This unit has no emergency: no new unit needed!");
		else {
			ArrayList<Unit> currentAvailableUnits = getEmergency().filterUnits(
					getHomeLocation());

			String typeUnit = this.getClass().toString();

			for (Unit u : currentAvailableUnits) {
				if (typeUnit.equalsIgnoreCase(u.getClass().toString())) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Abstract method to make check which unit type need help from the unit of
	 * the same type
	 * 
	 * @return Returns true if this unit can do it task at the emergency, false
	 *         if not
	 */
	protected abstract boolean unitsOfSameTypeOnEmergency();

	/**
	 * Method for updating the time of this unit, so it can move
	 * 
	 * @post This unit is moved, depending on the amount of minutes, the unit
	 *       traveled futher or closer
	 */
	public void update(Observable o, Object str) {
		Clock clock = (Clock) o;
		move(clock.getTimeDif());
	}

}
